/*
 * Copyright (c) 2023-2023 elsfs Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.elsfs.cloud.common.security.handler;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.JOSEObjectType;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.elsfs.cloud.api.security.key.Oauth2Token;
import org.elsfs.cloud.common.core.vo.R;
import org.elsfs.cloud.common.mybatis.properties.MybatisPlusProperties;
import org.elsfs.cloud.tenant.api.handler.TenantRequestContext;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * 处理登录是json请求格式的 成功或则失败Handler,
 *
 * @author zeng
 */
@RequiredArgsConstructor
public class JsonAuthenticationHandler implements LoginSuccessOrFailureHandler {

  private final ObjectMapper objectMapper;
  private final JwtEncoder jwtEncoder;
  private final String issuer;
  private final MybatisPlusProperties mybatisPlusProperties;
  private static final Duration TOKEN_TIME_TO_LIVE = Duration.ofDays(1L);

  /**
   * 认证失败处理
   *
   * @param request the request during which the authentication attempt occurred.
   * @param response the response.
   * @param exception the exception which was thrown to reject the authentication request.
   * @throws IOException e
   * @throws ServletException s
   */
  @Override
  public void onAuthenticationFailure(
      HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
      throws IOException, ServletException {
    R<Object> errorData = R.error(exception.getMessage());
    writeHttpServletResponse(response, errorData);
  }

  @Override
  public Mono<Void> onAuthenticationFailure(
      WebFilterExchange webFilterExchange, AuthenticationException exception) {
    R<Object> errorData = R.error(exception.getMessage());
    ServerWebExchange exchange = webFilterExchange.getExchange();
    ServerHttpResponse response = exchange.getResponse();
    response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
    try {
      return response.writeWith(
          Mono.just(
              response
                  .bufferFactory()
                  .wrap(
                      objectMapper
                          .writeValueAsString(errorData)
                          .getBytes(StandardCharsets.UTF_8))));
    } catch (JsonProcessingException e) {
      throw new RuntimeException(e);
    }
  }

  protected void writeHttpServletResponse(HttpServletResponse response, R<?> r) throws IOException {
    var outputStream = response.getOutputStream();
    response.setContentType(MimeTypeUtils.APPLICATION_JSON_VALUE);
    outputStream.write(objectMapper.writeValueAsString(r).getBytes(StandardCharsets.UTF_8));
    outputStream.flush();
  }

  /**
   * 认证成功处理
   *
   * @param request the request which caused the successful authentication
   * @param response the response
   * @param authentication the <tt>Authentication</tt> object which was created during the
   *     authentication process.
   * @throws IOException e
   */
  @Override
  public void onAuthenticationSuccess(
      HttpServletRequest request, HttpServletResponse response, Authentication authentication)
      throws IOException {
    Object principal = authentication.getPrincipal();
    String username = null;
    Set<String> scope = null;
    if (principal instanceof UserDetails userDetails) {
      username = userDetails.getUsername();
      scope = AuthorityUtils.authorityListToSet(userDetails.getAuthorities());
    }
    Jwt jwt = generatorToken(username, scope, request);
    Oauth2Token token =
        new Oauth2Token(
            Oauth2Token.TokenType.Bearer, jwt.getTokenValue(), jwt.getExpiresAt(), null, scope);
    writeHttpServletResponse(response, R.success(token));
  }

  /**
   * 创建 token
   *
   * @param username 用户名
   * @param scope 权限
   * @param request request
   * @return Jwt
   */
  protected Jwt generatorToken(String username, Set<String> scope, HttpServletRequest request) {
    var jwtClaimsSet =
        JwtClaimsSet.builder()
            .issuer(issuer)
            .issuedAt(Instant.now()) // jwt的发放时间，通常写当前时间的时间戳
            .expiresAt(Instant.now().plus(TOKEN_TIME_TO_LIVE)) // jwt的到期时间，通常写时间戳
            .notBefore(Instant.now()) // 一个时间点，在该时间点到达之前，这个令牌是不可用的
            //   .id() //
            // jwt的唯一编号，设置此项的目的，主要是为了防止重放攻击（重放攻击是在某些场景下，用户使用之前的令牌发送到服务器，被服务器正确的识别，从而导致不可预期的行为发生）
            .audience(Collections.singletonList("login")) // 该jwt是发放给哪个终端的，可以是终端类型，也可以是用户名称，随意一点
            .subject(username) //  sub字段的格式可以根据具体的业务需求进行定制。例如，在一些开放标准中，如OpenID
            // Connect，sub字段的值通常是一个URL，指向一个用户信息资源的地址。在一个JWT中，可以将sub字段设为用户的邮箱地址或其他唯一标识符，如用户ID或用户名。
            .claim(OAuth2ParameterNames.SCOPE, scope)
            .claim(
                IdTokenClaimNames.NONCE,
                request.getSession().getId()) //   nonce-一个字符串值，用于将客户端会话与ID令牌相关联，并减轻重放攻击。
            .build();
    JwsHeader.Builder jwsHeaderBuilder =
        JwsHeader.with(SignatureAlgorithm.RS512).type(JOSEObjectType.JWT.getType());
    if (mybatisPlusProperties.getTenant().getEnabled()) {
      jwsHeaderBuilder.keyId(TenantRequestContext.getTenantLocal());
    }
    JwtEncoderParameters jwtEncoderParameters =
        JwtEncoderParameters.from(jwsHeaderBuilder.build(), jwtClaimsSet);
    return jwtEncoder.encode(jwtEncoderParameters);
  }

  /** 没有权限 */
  @Override
  public void handle(
      HttpServletRequest request,
      HttpServletResponse response,
      AccessDeniedException accessDeniedException)
      throws IOException, ServletException {
    R<Object> errorData = R.error("没有权限访问");
    writeHttpServletResponse(response, errorData);
  }

  @Override
  public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {
    R<Object> errorData = R.error("没有权限访问");
    ServerHttpResponse response = exchange.getResponse();
    response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

    try {
      byte[] bytes = objectMapper.writeValueAsString(errorData).getBytes(StandardCharsets.UTF_8);
      return response.writeWith(
          Mono.fromSupplier(() -> new DefaultDataBufferFactory().wrap(bytes)));
    } catch (JsonProcessingException e) {
      throw new RuntimeException(e);
    }
  }
}
